# -*- coding: utf-8 -*-
"""
@Time    : 2022/9/14 15:06
@Author  : huanghuang
"""
import base64
import datetime
import json
import re
import time
from typing import Union
from urllib.parse import quote_plus

import rsa
import ujson
from Crypto.Cipher import AES
from starlette.datastructures import FormData

from common.g.g import G_LOGGER
from common.pkg.req import Client
from common.pkg.str_id import get_short_uuid
from schema.inner_use_schema.ali_pay_schema import AliPayPageRequestModel, AliPayAppRequestModel, AliPayWapRequestModel, \
    AliPayTradeQueryRequestModel, AliPayRefundRequestModel, AliPayRefundQueryRequestModel, AliPayTradeCloseRequestModel


__all__ = ["AliClient"]

COMMON_PARAM_KEYS = set()

P_APP_ID = "app_id"
COMMON_PARAM_KEYS.add(P_APP_ID)

P_METHOD = "method"
COMMON_PARAM_KEYS.add(P_METHOD)

P_FORMAT = "format"
COMMON_PARAM_KEYS.add(P_FORMAT)

P_CHARSET = "charset"
COMMON_PARAM_KEYS.add(P_CHARSET)

P_SIGN_TYPE = "sign_type"
COMMON_PARAM_KEYS.add(P_SIGN_TYPE)

P_SIGN = "sign"
COMMON_PARAM_KEYS.add(P_SIGN)

P_ENCRYPT_TYPE = "encrypt_type"
COMMON_PARAM_KEYS.add(P_ENCRYPT_TYPE)

P_TIMESTAMP = "timestamp"
COMMON_PARAM_KEYS.add(P_TIMESTAMP)

P_VERSION = "version"
COMMON_PARAM_KEYS.add(P_VERSION)

P_NOTIFY_URL = "notify_url"
COMMON_PARAM_KEYS.add(P_NOTIFY_URL)

P_RETURN_URL = "return_url"
COMMON_PARAM_KEYS.add(P_RETURN_URL)

P_AUTH_TOKEN = "auth_token"
COMMON_PARAM_KEYS.add(P_AUTH_TOKEN)

P_APP_AUTH_TOKEN = "app_auth_token"
COMMON_PARAM_KEYS.add(P_APP_AUTH_TOKEN)

P_BIZ_CONTENT = "biz_content"

BLOCK_SIZE = AES.block_size
pad = lambda s, length: s + (BLOCK_SIZE - length % BLOCK_SIZE) * chr(BLOCK_SIZE - length % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]

PATTERN_RESPONSE_BEGIN = re.compile(r'(\"[a-zA-z_]+_response\"[ \t\n]*:[ \t\n]*\{)')
PATTERN_RESPONSE_SIGN_BEGIN = re.compile(r'(\}[ \t\n]*,[ \t\n]*\"sign\"[ \t\n]*:[ \t\n]*\")')

PATTERN_RESPONSE_ENCRYPT_BEGIN = re.compile(r'(\"[a-zA-z_]+_response\"[ \t\n]*:[ \t\n]*\")')
PATTERN_RESPONSE_SIGN_ENCRYPT_BEGIN = re.compile(r'(\"[ \t\n]*,[ \t\n]*\"sign\"[ \t\n]*:[ \t\n]*\")')


def has_value(m, key):
    if not m:
        return False
    if not (key in m):
        return False
    if not m[key]:
        return False
    return True


def get_sign_content(all_params):
    G_LOGGER.info(f"带签名数据:{all_params}")
    sign_content = ""
    for (k, v) in sorted(all_params.items()):
        value = v
        if not isinstance(value, str):
            value = json.dumps(value, ensure_ascii=False, separators=(',', ':'))
        sign_content += ("&" + k + "=" + value)
    sign_content = sign_content[1:]
    G_LOGGER.info(f"待签名数据:{sign_content}")
    return sign_content


def add_start_end(key, startMarker, endMarker):
    if key.find(startMarker) < 0:
        key = startMarker + key
    if key.find(endMarker) < 0:
        key = key + endMarker
    return key


def fill_private_key_marker(private_key):
    return add_start_end(private_key, "-----BEGIN RSA PRIVATE KEY-----\n", "\n-----END RSA PRIVATE KEY-----")


def fill_public_key_marker(public_key):
    return add_start_end(public_key, "-----BEGIN PUBLIC KEY-----\n", "\n-----END PUBLIC KEY-----")


def sign_with_rsa(private_key, sign_content, charset):
    sign_content = sign_content.encode(charset)
    private_key = fill_private_key_marker(private_key)
    signature = rsa.sign(sign_content, rsa.PrivateKey.load_pkcs1(private_key, format='PEM'), 'SHA-1')
    sign = base64.b64encode(signature)
    sign = str(sign, encoding=charset)
    return sign


def sign_with_rsa2(private_key: str, sign_content: str, charset: str):
    sign_content = sign_content.encode(charset)
    private_key = fill_private_key_marker(private_key)
    signature = rsa.sign(sign_content, rsa.PrivateKey.load_pkcs1(private_key, format='PEM'), 'SHA-256')
    sign = base64.b64encode(signature)
    sign = str(sign, encoding=charset)
    return sign


def verify_with_rsa(public_key: str, message: bytes, sign: Union[str, bytes]):
    public_key = fill_public_key_marker(public_key)
    sign = base64.b64decode(sign)
    return bool(rsa.verify(message, sign, rsa.PublicKey.load_pkcs1_openssl_pem(public_key)))


def url_encode(params: dict, charset: str):
    query_string = ""
    for (k, v) in params.items():
        value = v
        if not isinstance(value, str):
            value = json.dumps(value, ensure_ascii=False, separators=(',', ':'))
        value = quote_plus(value, encoding=charset)
        query_string += ("&" + k + "=" + value)
    query_string = query_string[1:]
    return query_string


def encrypt_content(content, encrypt_type, encrypt_key, charset):
    if "AES" == encrypt_type.upper():
        return aes_encrypt_content(content, encrypt_key, charset)
    raise RuntimeError("当前不支持该算法类型encrypt_type=" + encrypt_type)


def aes_encrypt_content(content, encrypt_key, charset):
    length = len(bytes(content, encoding=charset))
    padded_content = pad(content, length)
    iv = '\0' * BLOCK_SIZE
    cryptor = AES.new(base64.b64decode(encrypt_key), AES.MODE_CBC, iv)
    encrypted_content = cryptor.encrypt(padded_content)
    encrypted_content = base64.b64encode(encrypted_content)
    encrypted_content = str(encrypted_content, encoding=charset)
    return encrypted_content


def decrypt_content(encrypted_content, encrypt_type, encrypt_key, charset):
    if "AES" == encrypt_type.upper():
        return aes_decrypt_content(encrypted_content, encrypt_key, charset)
    raise RuntimeError("当前不支持该算法类型encrypt_type=" + encrypt_type)


def aes_decrypt_content(encrypted_content, encrypt_key, charset):
    encrypted_content = base64.b64decode(encrypted_content)
    iv = '\0' * BLOCK_SIZE
    cryptor = AES.new(base64.b64decode(encrypt_key), AES.MODE_CBC, iv)
    content = unpad(cryptor.decrypt(encrypted_content))
    content = content.decode(charset)
    return content


class AliBase:
    def __init__(self,
                 client: Client,
                 appid: str,
                 app_private_key: str,
                 ali_public_key: str,
                 domain: str = "https://openapi.alipay.com/gateway.do",
                 encrypt_type: str = None,
                 encrypt_key: str = None,
                 return_url: str = None,
                 notify_url: str = None,
                 sign_type: str = "RSA2",
                 charset: str = "utf-8",
                 ):
        self.__domain = domain
        self.__client = client
        self.__appid = appid
        self.__app_private_key = app_private_key
        self.__ali_public_key = ali_public_key
        self.__encrypt_type = encrypt_type
        self.__encrypt_key = encrypt_key
        self.__return_url = return_url
        self.__notify_url = notify_url
        self.__sign_type = sign_type
        self.__charset = charset
        self.__format = "JSON"
        self.__version = "1.0"

    @property
    def domain(self):
        return self.__domain

    @property
    def client(self):
        return self.__client

    @property
    def appid(self):
        return self.__appid

    @property
    def app_private_key(self):
        return self.__app_private_key

    @property
    def ali_public_key(self):
        return self.__ali_public_key

    @property
    def encrypt_type(self):
        return self.__encrypt_type

    @property
    def encrypt_key(self):
        return self.__encrypt_key

    @property
    def return_url(self):
        return self.__return_url

    @property
    def notify_url(self):
        return self.__notify_url

    @property
    def sign_type(self):
        return self.__sign_type

    @property
    def charset(self):
        return self.__charset

    @property
    def format(self):
        return self.__format

    @property
    def version(self):
        return self.__version

    def __get_common_params(self, params: dict):
        common_params = dict()
        common_params[P_TIMESTAMP] = params[P_TIMESTAMP]
        common_params[P_APP_ID] = self.appid
        common_params[P_METHOD] = params[P_METHOD]
        common_params[P_CHARSET] = self.charset
        common_params[P_FORMAT] = self.format
        common_params[P_VERSION] = params[P_VERSION]
        common_params[P_SIGN_TYPE] = self.sign_type
        # if self.__config.encrypt_type:
        #     common_params[P_ENCRYPT_TYPE] = self.__config.encrypt_type
        if has_value(params, P_APP_AUTH_TOKEN):
            common_params[P_APP_AUTH_TOKEN] = params[P_APP_AUTH_TOKEN]
        if has_value(params, P_AUTH_TOKEN):
            common_params[P_AUTH_TOKEN] = params[P_AUTH_TOKEN]
        if has_value(params, P_NOTIFY_URL):
            common_params[P_NOTIFY_URL] = params[P_NOTIFY_URL]
        if has_value(params, P_RETURN_URL):
            common_params[P_RETURN_URL] = params[P_RETURN_URL]
        return common_params

    @staticmethod
    def __remove_common_params(params):
        if not params:
            return
        for k in COMMON_PARAM_KEYS:
            if k in params:
                params.pop(k)

    def __prepare_request_params(self,
                                 params,
                                 need_encrypt: bool = False):
        if P_BIZ_CONTENT in params:
            if self.encrypt_type and self.encrypt_key:
                params[P_BIZ_CONTENT] = encrypt_content(params[P_BIZ_CONTENT], self.encrypt_type,
                                                        self.encrypt_key, self.charset)
            else:
                if need_encrypt:
                    raise RuntimeError("接口" + params[P_METHOD] + "必须使用encrypt_type、encrypt_key加密")
        params[P_TIMESTAMP] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        common_params = self.__get_common_params(params)
        all_params = dict()
        all_params.update(params)
        all_params.update(common_params)
        sign_content = get_sign_content(all_params)
        try:
            if self.sign_type and self.sign_type == 'RSA2':
                sign = sign_with_rsa2(self.app_private_key, sign_content, self.charset)
            else:
                sign = sign_with_rsa(self.app_private_key, sign_content, self.charset)
        except Exception as e:
            raise e from None
        G_LOGGER.info(f"sign={sign}")
        common_params[P_SIGN] = sign
        self.__remove_common_params(params)

        return common_params, params

    def prepare_request(self, **origin_params):
        G_LOGGER.info(f"origin_params={origin_params}")
        common_params, params = self.__prepare_request_params(origin_params)
        query_string = url_encode(common_params, self.charset)
        return query_string, params

    def parse_response(self, response_str):
        response_str = response_str.decode(self.charset)
        response_content = None
        sign = None
        em1 = None
        em2 = None
        has_encrypted = False
        if self.encrypt_type and self.encrypt_key:
            em1 = PATTERN_RESPONSE_ENCRYPT_BEGIN.search(response_str)
            em2 = PATTERN_RESPONSE_SIGN_ENCRYPT_BEGIN.search(response_str)
            if em1 and em2:
                has_encrypted = True
                sign_start_index = em2.start()
                sign_end_index = em2.end()
                while em2:
                    em2 = PATTERN_RESPONSE_SIGN_BEGIN.search(response_str, pos=em2.end())
                    if em2:
                        sign_start_index = em2.start()
                        sign_end_index = em2.end()
                response_content = response_str[em1.end() - 1:sign_start_index + 1]
                response_content = response_content.encode(self.charset)
                sign = response_str[sign_end_index:response_str.find("\"", sign_end_index)]
        if not response_content:
            m1 = PATTERN_RESPONSE_BEGIN.search(response_str)
            m2 = PATTERN_RESPONSE_SIGN_BEGIN.search(response_str)
            if not m1 or not m2:
                raise RuntimeError(f"非法响应:{response_str}")
            sign_start_index = m2.start()
            sign_end_index = m2.end()
            while m2:
                m2 = PATTERN_RESPONSE_SIGN_BEGIN.search(response_str, pos=m2.end())
                if m2:
                    sign_start_index = m2.start()
                    sign_end_index = m2.end()
            response_content = response_str[m1.end() - 1:sign_start_index + 1]
            response_content = response_content.encode(self.charset)
            sign = response_str[sign_end_index:response_str.find("\"", sign_end_index)]
        try:
            verify_res = verify_with_rsa(self.ali_public_key, response_content, sign)
        except Exception as e:
            raise RuntimeError(f"response sign verify failed {e} \n{response_str}")
        if not verify_res:
            raise RuntimeError(f"response sign verify failed \n{response_str}")
        response_content = response_content.decode(self.charset)

        if has_encrypted:
            response_content = decrypt_content(response_content[1:-1], self.encrypt_type,
                                               self.encrypt_key, self.charset)
        return response_content

    def _get_default_header(self):
        return {
            'Content-type': 'application/x-www-form-urlencoded;charset=' + self.charset,
            "Cache-Control": "no-cache",
            "Connection": "Keep-Alive"
        }

    async def _request(self, **kwargs):
        query_string, params = self.prepare_request(**kwargs)
        parse = kwargs.pop('parse', True)  # 是否需要对结果进行解析
        body = url_encode(params, self.charset)
        headers = self._get_default_header()
        full_url = f"{self.domain}?{query_string}"
        request_id = get_short_uuid(prefix="ali_")
        start_time = time.time()
        G_LOGGER.info(f"请求ali接口:request_id={request_id} method={kwargs.get('method')} full_url={full_url}")
        response = await self.client.post(url=full_url, data=body, parse_with_json=False, headers=headers)
        # response = await self.client.post(url=full_url, parse_with_json=False, headers=headers)
        G_LOGGER.info(
            f"请求ali接口:request_id={request_id}\ntime:{1000 * (time.time() - start_time)}ms\nresponse={response}")
        if isinstance(response, dict):  # 如果是dict表示出错了
            return False, response
        if parse:
            return True, ujson.loads(self.parse_response(response))
        try:
            decode_response = response.decode(self.charset)
        except Exception:
            decode_response = response.decode("GBK")
        return True, decode_response

    def verify_async_response(self, data: Union[dict, FormData]) -> bool:
        """
        对异步结果验签
        文档地址： https://opendocs.alipay.com/open/270/105902
        :param data: 异步form表单或者dict数据
        :return:
        """
        # 剔除sign和sign_type
        data = dict(data)
        sign = data.pop("sign")
        data.pop("sign_type", None)
        # 获取待签名字符串
        sign_content = get_sign_content(data)
        sign_content = sign_content.encode(self.charset)
        return verify_with_rsa(self.ali_public_key, sign_content, sign)


class AliClient(AliBase):

    # 电脑网站支付
    async def page_pay(self, data: AliPayPageRequestModel):
        """
        文档地址： https://opendocs.alipay.com/open/028r8t?scene=22
        :param data:
        :return:
        """
        ali_method = "alipay.trade.page.pay"
        return await self._request(biz_content=data.dict(exclude_none=True),
                                   version=self.version,
                                   method=ali_method,
                                   notify_url=self.notify_url,
                                   return_url=self.return_url,
                                   parse=False
                                   )

    # app支付
    async def app_pay(self, data: AliPayAppRequestModel):
        """
        文档地址: https://opendocs.alipay.com/open/02e7gq?scene=20
        :param data:
        :return:
        """
        ali_method = "alipay.trade.app.pay"
        return await self._request(biz_content=data.dict(exclude_unset=True, exclude_none=True),
                                   version=self.version,
                                   method=ali_method,
                                   notify_url=self.notify_url,
                                   return_url=self.return_url
                                   )

    # 手机网站支付
    async def wap_pay(self, data: AliPayWapRequestModel):
        """
        文档地址：https://opendocs.alipay.com/open/02ivbs?scene=21
        :param data:
        :return:
        """
        ali_method = "alipay.trade.wap.pay"
        return await self._request(biz_content=data.dict(exclude_unset=True, exclude_none=True),
                                   version=self.version,
                                   method=ali_method,
                                   notify_url=self.notify_url,
                                   return_url=self.return_url
                                   )

    # 交易查询
    async def trade_pay_query(self, data: AliPayTradeQueryRequestModel):
        """
        需要调用查询接口的情况：
        当商户后台、网络、服务器等出现异常，商户系统最终未接收到支付通知；
        调用支付接口后，返回系统错误或未知交易状态情况；
        调用alipay.trade.pay，返回INPROCESS的状态；
        调用alipay.trade.cancel之前，需确认支付状态；
        文档地址: https://opendocs.alipay.com/open/02e7gm
        :param data:
        :return:
        """
        ali_method = "alipay.trade.query"
        return await self._request(biz_content=data.dict(exclude_unset=True, exclude_none=True),
                                   version=self.version,
                                   method=ali_method
                                   )

    # 退款
    async def trade_refund(self, data: AliPayRefundRequestModel):
        """
        1. 同一笔交易的退款至少间隔3s后发起
        2. 请严格按照接口文档中的参数进行接入。若在此接口中传入【非当前接口文档中的参数】会造成【退款失败或重复退款】。
        3. 该接口不可与其他退款产品混用。若商户侧同一笔退款请求已使用了当前接口退款的情况下，【再使用其他退款产品进行退款】可能会造成【重复退款】。
        4. 退款成功判断说明：接口返回fund_change=Y为退款成功，fund_change=N或无此字段值返回时需通过退款查询接口进一步确认退款状态。详见退款成功判断指导。注意，接口中code=10000，仅代表本次退款请求成功，不代表退款成功。
        文档地址: https://opendocs.alipay.com/open/02e7go
        :param data:
        :return:
        """
        ali_method = "alipay.trade.refund"
        return await self._request(biz_content=data.dict(exclude_unset=True, exclude_none=True),
                                   version=self.version,
                                   method=ali_method
                                   )

    # 退款查询
    async def trade_refund_query(self, data: AliPayRefundQueryRequestModel):
        """
        1. 该接口的返回码10000，仅代表本次查询操作成功，不代表退款成功，当接口返回的refund_status值为REFUND_SUCCESS时表示退款成功，否则表示退款没有执行成功。
        2. 如果退款未成功，商户可以调用退款接口重试，重试时请务必保证退款请求号和退款金额一致，防止重复退款。
        3. 发起退款查询接口的时间不能离退款请求时间太短，建议之间间隔10秒以上。
        文档地址: https://opendocs.alipay.com/open/02e7gp
        :param data:
        :return:
        """
        ali_method = "alipay.trade.fastpay.refund.query"
        return await self._request(biz_content=data.dict(exclude_unset=True, exclude_none=True),
                                   version=self.version,
                                   method=ali_method
                                   )

    # 关单
    async def trade_close(self, data: AliPayTradeCloseRequestModel):
        """
        用于交易创建后，用户在一定时间内未进行支付，可调用该接口直接将未付款的交易进行关闭。
        文档地址: https://opendocs.alipay.com/open/02e7gn
        :param data:
        :return:
        """
        ali_method = "alipay.trade.close"
        return await self._request(biz_content=data.dict(exclude_unset=True, exclude_none=True),
                                   version=self.version,
                                   method=ali_method
                                   )

#
# class AliClientFactory:
#
#     def __init__(self):
#         self.__lock = Lock()
#         self.__factory = dict()
#
#     @property
#     def factory(self) -> Dict[str, AliClient]:
#         """
#         {
#             "appid_1": client1,
#             "appid_2": client2
#         }
#         """
#         return self.__factory
#
#     @property
#     def lock(self):
#         return self.__lock
#
#     def get_client(self, appid) -> AliClient:
#         return self.factory.get(appid)
#
#     def init_client(self, **config) -> AliClient:
#         """
#         config: 应该是一个MerchantAliPayConfig的配置信息
#         """
#         client = AliClient(client=G_CLIENT, **config)
#         self.factory[config["appid"]] = client
#         return client
#
#     async def init_by_appid(self, appid: str, db: Manager = None) -> AliClient:
#         from common.pkg.db_utils.db_util import GLOBAL_DEFAULT_DB
#         from common.models.merchant.pay_config import MerchantAliPayConfig
#         db = db or GLOBAL_DEFAULT_DB
#         ali_config_query = MerchantAliPayConfig.select().where(MerchantAliPayConfig.appid==appid).dicts()
#         try:
#             res = await db.get(ali_config_query)
#         except:
#             raise ValueError(f"支付宝配置:{appid}不存在")
#         async with self.lock:
#             return self.init_client(**res)
#
#     async def compare_and_reset_client(self, **new_data):
#         """
#         比较配置 如果配置信息发生改变，则创建新的client对象
#         """
#
#         appid = new_data["appid"]
#         async with self.lock:
#             client = self.get_client(appid)
#             if not client:
#                 client = self.init_client(**new_data)
#                 self.factory[appid] = client
#                 G_LOGGER.info(f"新增支付宝支付:{appid}->{new_data}")
#             else:
#                 changed, old_data, new_data = self.is_config_changed(appid, client, new_data)
#                 if changed:
#                     s = self._find_change(old_data, new_data)
#                     G_LOGGER.info(f"支付宝配置信息发生改变:\n{s}")
#                 new_client = AliClient(client=client.client, **new_data)
#                 self.factory[appid] = new_client
#
#     def is_config_changed(self, appid, client, new_data):
#         """
#         判断数据是否有发生改变
#         如果发生改变，则返回True 否则返回False
#         """
#         data = {}
#         data["appid"] = appid
#         data["app_private_key"] = client.app_private_key
#         data["ali_public_key"] = client.ali_public_key
#         data["domain"] = client.domain
#         data["encrypt_key"] = client.encrypt_key
#         data["encrypt_type"] = client.encrypt_type
#         data["return_url"] = client.return_url
#         data["notify_url"] = client.notify_url
#         data["sign_type"] = client.sign_type
#         data["charset"] = client.charset
#         return data != new_data, data, new_data,
#
#     def _find_change(self, old_data, new_data):
#         s = ""
#         for k, v in old_data.items():
#             if v != new_data.get(k):
#                 s += f"{v} -> {new_data[k]}\n"
#         return s
#
#
# def init_ali_factory():
#     global GLOBAL_ALI_CLIENT_FACTORY
#     GLOBAL_ALI_CLIENT_FACTORY = AliClientFactory()
#     return GLOBAL_ALI_CLIENT_FACTORY
#
#
# GLOBAL_ALI_CLIENT_FACTORY: AliClientFactory
